package core

import (
	"errors"
	"fmt"
)

func Eval(expr Expression, env *Environment) (Object, error) {
	switch expr := expr.(type) {
	case *SymbolNode:
		return evalSymbol(expr, env)
	case *BooleanNode:
		return evalBoolean(expr, env)
	case *ListNode:
		return evalList(expr, env)
	default:
		return nil, errors.New("Invalid expression")
	}
}

func evalSymbol(expr *SymbolNode, env *Environment) (Object, error) {
	expr.value = expr.token.Lexeme
	val, ok := env.Get(expr.value)
	if !ok {
		return nil, fmt.Errorf("undefined variable: %s", expr.token.Lexeme)
	}
	return val, nil
}

func evalBoolean(expr *BooleanNode, env *Environment) (Object, error) {
	if expr.token.Lexeme == "#f" {
		expr.value = false
	} else {
		expr.value = true
	}

	return &BooleanObject{expr.value}, nil
}
func evalList(expr *ListNode, env *Environment) (Object, error) {
	if len(expr.nodes) == 0 {
		return &NullObject{}, nil
	}
	if expr.nodes[0].Type() == SYMBOL_NODE {
		switch expr.nodes[0].(*SymbolNode).token.Lexeme {
		case "define":
			return applyDefine(expr.nodes[1:], env)
		case "let":
			return applyLet(expr.nodes[1:], env)
		default:
			return evalProcedure(expr, env)
		}
	}
	return nil, fmt.Errorf("attempt to apply non-procedure: %s", expr.nodes[0].String())
}

func evalProcedure(expr *ListNode, env *Environment) (Object, error) {
	fname := expr.nodes[0].(*SymbolNode).token.Lexeme
	f, ok := env.Get(fname)
	if !ok {
		return nil, fmt.Errorf("undefined: %s", fname)
	}
	return applyProcedure(f, expr.nodes[1:], env)
}

func applyProcedure(f Object, args []Node, env *Environment) (Object, error) {
	objs := make([]Object, len(args))
	for i, arg := range args {
		if obj, err := Eval(arg, env); err != nil {
			return nil, err
		} else {
			objs[i] = obj
		}
	}

	switch proc := f.(type) {
	case *Builtin:
		result := proc.Fn(objs, env)
		if result.Type() == ERROR_OBJ {
			return nil, errors.New(result.Inspect())
		}
		return result, nil
	case *Procedure:
		// Create a new environment for the procedure call
		newEnv := NewEnclosedEnvironment(proc.Env)

		// Bind parameters to arguments
		if len(proc.Params) != len(objs) {
			return nil, fmt.Errorf("wrong number of arguments to %s: expected %d, got %d",
				proc.Name, len(proc.Params), len(objs))
		}

		for i, param := range proc.Params {
			newEnv.Set(param, objs[i])
		}

		// Evaluate the procedure body
		var result Object
		var err error
		for _, expr := range proc.Body {
			result, err = Eval(expr, newEnv)
			if err != nil {
				return nil, err
			}
		}

		return result, nil
	default:
		return nil, fmt.Errorf("attempt to apply non-procedure")
	}
}

func applyDefine(args []Expression, env *Environment) (Object, error) {
	// Check if we have at least 2 arguments
	if len(args) < 2 {
		return nil, errors.New("define requires at least 2 arguments")
	}

	// Handle(define name value) form - first argument is a symbol
	if nameExpr, ok := args[0].(*SymbolNode); ok {
		// Evaluate the value expression
		if value, err := Eval(args[1], env); err != nil {
			return nil, err
		} else {
			// In the environment, set the variable
			env.Set(nameExpr.token.Lexeme, value)
			return NULL_OBJECT, nil
		}
	}

	// Handle(define (name arg1 arg2 ...) body) form - first argument is a list
	if procDefExpr, ok := args[0].(*ListNode); ok {
		if len(procDefExpr.nodes) < 1 {
			return nil, errors.New("invalid procedure definition in define")
		}

		// Check if the procedure name is a symbol
		procNameExpr, ok := procDefExpr.nodes[0].(*SymbolNode)
		if !ok {
			return nil, errors.New("procedure name must be a symbol")
		}

		// Extract parameters
		var params []string
		for i := 1; i < len(procDefExpr.nodes); i++ {
			paramExpr, ok := procDefExpr.nodes[i].(*SymbolNode)
			if !ok {
				return nil, errors.New("procedure parameters must be symbols")
			}
			params = append(params, paramExpr.token.Lexeme)
		}

		// Create a procedure object
		proc := &Procedure{
			Name:   procNameExpr.token.Lexeme,
			Params: params,
			Body:   args[1:], // The body consists of all expressions after the procedure definition
			Env:    env,
		}

		// In the environment, set the procedure
		env.Set(procNameExpr.token.Lexeme, proc)
		return proc, nil
	}

	return nil, errors.New("define expects a symbol or a procedure definition as the first argument")
}

func applyLet(args []Expression, env *Environment) (Object, error) {
	// Check if we have at least the bindings and one body expression
	if len(args) < 2 {
		return nil, errors.New("let requires at least bindings and one body expression")
	}

	// First argument should be the bindings list
	bindings, ok := args[0].(*ListNode)
	if !ok {
		return nil, errors.New("let bindings must be a list")
	}

	// Create a new enclosed environment
	newEnv := NewEnclosedEnvironment(env)

	// Process each binding
	for _, binding := range bindings.nodes {
		// Each binding should be a list of (var value)
		bindingList, ok := binding.(*ListNode)
		if !ok {
			return nil, errors.New("let binding must be a list")
		}

		// Check if binding has exactly two elements
		if len(bindingList.nodes) != 2 {
			return nil, errors.New("let binding must have exactly two elements: variable and value")
		}

		// First element should be a symbol (variable name)
		varNode, ok := bindingList.nodes[0].(*SymbolNode)
		if !ok {
			return nil, errors.New("let variable must be a symbol")
		}

		// Second element is the value expression
		valExpr := bindingList.nodes[1]

		// Evaluate the value expression in the current environment (outer scope)
		val, err := Eval(valExpr, env)
		if err != nil {
			return nil, err
		}

		// Bind the variable to the value in the new environment
		newEnv.Set(varNode.token.Lexeme, val)
	}

	/*
		// Evaluate body expressions in the new environment
		var result Object
		var err error
		for i := 1; i < len(args); i++ {
			result, err = Eval(args[i], newEnv)
			if err != nil {
				return nil, err
			}
		}
	*/
	return EvalProgram(args[1:], newEnv)
}

func EvalProgram(program []Expression, env *Environment) (Object, error) {
	var result Object
	var err error
	for _, expr := range program {
		result, err = Eval(expr, env)
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}
